{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%reset -f"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Voxel painter in pythreejs\n",
    "Based on: [voxel painter](http://threejs.org/examples/webgl_interactive_voxelpainter.html) ([source](https://github.com/mrdoob/three.js/blob/master/examples/webgl_interactive_voxelpainter.html))\n",
    "\n",
    "TODO: \n",
    "    - Have rollOver helper snap to other voxel face\n",
    "    - Delete voxel when shift-clicked"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import division, print_function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pythreejs import *\n",
    "from IPython.display import display\n",
    "from ipywidgets import HTML\n",
    "from traitlets import link"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "import math\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Geometry definitions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "csize = 50 # size of voxel\n",
    "nx = ny = 10\n",
    "stepx, stepy = csize, csize\n",
    "sizex, sizey = nx * csize, ny * csize"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Helper functions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def normalize(list):\n",
    "    \"\"\" \n",
    "    Normalize vector list \n",
    "    \"\"\"\n",
    "    return [x/sum(list) for x in list]\n",
    "\n",
    "def rotation_matrix(angle, axis='x'):\n",
    "    \"\"\" \n",
    "    Return rotation matrix as list of rows used in the\n",
    "    Object3d.quaternion_from_rotation() class method\n",
    "    \"\"\"\n",
    "    if axis in ['x','y','z']:\n",
    "        sin = math.sin(angle)\n",
    "        cos = math.cos(angle)\n",
    "        # counter-clockwise rotation in yz-plane\n",
    "        if axis is 'x':\n",
    "            return [1, 0, 0, 0, cos, -sin, 0, sin, cos]\n",
    "        # counter-clockwise rotation in xz-plane\n",
    "        elif axis is 'y':\n",
    "            return [cos, 0, sin, 0, 1, 0, -sin, 0, cos]\n",
    "        # counter-clockwise rotation in xy-plane\n",
    "        elif axis is 'z':\n",
    "            return [cos, -sin, 0, sin, cos, 0, 0, 0, 1]\n",
    "    else:\n",
    "        raise ValueError('Cannot rotate about %s axis' % axis)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Scene objects:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Voxels"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cube_tex = ImageTexture(\n",
    "    imageUri = 'textures/square-outline-textured.png'\n",
    ")\n",
    "\n",
    "cube_geo = BoxGeometry(\n",
    "    width = csize,\n",
    "    height = csize,\n",
    "    depth = csize\n",
    ")\n",
    "\n",
    "cube_mat = MeshLambertMaterial(\n",
    "    color = '#feb74c',\n",
    "    shading = 'FlatShading',\n",
    "    map = cube_tex\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Roll-over helper"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rollOver_geo = cube_geo\n",
    "\n",
    "rollOver_mat = MeshBasicMaterial(\n",
    "    color = '#ff0000',\n",
    "    opacity = 0.5,\n",
    "    transparent = True\n",
    ")\n",
    "\n",
    "rollOver_point = Mesh(\n",
    "    geometry = rollOver_geo,\n",
    "    material = rollOver_mat,\n",
    "    visible = False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Surface"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surf_geo = SurfaceGeometry(\n",
    "    z = [0] * (nx + 1) * (ny + 1),\n",
    "    width = sizex,\n",
    "    height = sizey,\n",
    "    width_segments = nx,\n",
    "    height_segments = ny,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surf_grid = SurfaceGrid(\n",
    "    geometry = surf_geo,\n",
    "    material = LineBasicMaterial(\n",
    "        color = '#000000',\n",
    "        opacity = 0.2,\n",
    "        transparent = True,\n",
    "    ),\n",
    ")\n",
    "\n",
    "surface = Mesh(\n",
    "    geometry = surf_geo,\n",
    "    material = MeshBasicMaterial(\n",
    "        color = '#00ff00',\n",
    "        opacity = 0.3,\n",
    "        transparent = True,\n",
    "        side = 'DoubleSide',\n",
    "        \n",
    "    ),\n",
    "    visible = True\n",
    ")\n",
    "\n",
    "m = rotation_matrix(-math.pi/2)\n",
    "surface.setRotationFromMatrix(m)\n",
    "surf_grid.setRotationFromMatrix(m)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Pickers (raycasting)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pickable_objects = Group()\n",
    "pickable_objects.add(surface)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "click_picker = Picker(\n",
    "    controlling = pickable_objects,\n",
    "    event = 'dblclick'\n",
    ")\n",
    "mousemove_picker = Picker(\n",
    "    controlling = pickable_objects,\n",
    "    event = 'mousemove'\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def map_to_grid(value):\n",
    "    \"\"\"\n",
    "    Convert continous to discrete coordinates\n",
    "    \"\"\"\n",
    "    # limit position to positive y-axis\n",
    "    if value[1] < 0: \n",
    "        value[1] = float(0)\n",
    "        \n",
    "    # limit to discrete steps based on cube size\n",
    "    pos = [int(x//csize*csize+csize/2) for x in value]\n",
    "\n",
    "    # if block already exist at this position, shift up\n",
    "    while tuple(pos) in objects.keys():\n",
    "        pos[1] += csize\n",
    "    return pos\n",
    "\n",
    "def map_to_voxel_side(voxel, face):\n",
    "    \"\"\"\n",
    "    Convert continous to discrete coordinates\n",
    "    \"\"\"\n",
    "    # limit to discrete steps based on cube size\n",
    "    pos = list(voxel.position)\n",
    "    print(face)\n",
    "    if face in (0, 1):\n",
    "        pos[0] += csize\n",
    "    elif face in (2, 3):\n",
    "        pos[0] -= csize\n",
    "    elif face in (4, 5):\n",
    "        pos[1] += csize\n",
    "    elif face in (6, 7):\n",
    "        pos[1] -= csize\n",
    "    elif face in (8, 9):\n",
    "        pos[2] += csize\n",
    "    elif face in (10, 11):\n",
    "        pos[2] -= csize\n",
    "\n",
    "    # if block already exist at this position, mark as invalid\n",
    "    while tuple(pos) in objects.keys():\n",
    "        return None\n",
    "    return pos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "objects = {} # contains all voxels added to the scene\n",
    "\n",
    "def on_surf_click(change):\n",
    "    \"\"\"\n",
    "    Create new object when mouse is clicked on surface\n",
    "    \"\"\"\n",
    "    # convert position to discrete coordinates\n",
    "    pos = map_to_grid(change.owner.point)\n",
    "        \n",
    "    # create new object\n",
    "    voxel = Mesh(\n",
    "        geometry = cube_geo,\n",
    "        material = cube_mat,\n",
    "        position = pos\n",
    "    )\n",
    "    \n",
    "    # add new object to scene and object list\n",
    "    pickable_objects.add(voxel)\n",
    "    objects[tuple(map(int, voxel.position))] = voxel\n",
    "    \n",
    "def on_voxel_click(change):\n",
    "    \"\"\"\n",
    "    Create new object when mouse is clicked on voxel\n",
    "    \"\"\"\n",
    "    old_voxel = change.owner.object\n",
    "    if change.owner.modifiers[0] is True:\n",
    "        pickable_objects.remove(old_voxel)\n",
    "        del objects[tuple(map(int, old_voxel.position))]\n",
    "        return\n",
    "    # convert position to discrete coordinates\n",
    "    pos = map_to_voxel_side(old_voxel, change.owner.faceIndex)\n",
    "    if pos is None:\n",
    "        return\n",
    "        \n",
    "    # create new object\n",
    "    voxel = Mesh(\n",
    "        geometry = cube_geo,\n",
    "        material = cube_mat,\n",
    "        position = pos\n",
    "    )\n",
    "    \n",
    "    # add new object to scene and object list\n",
    "    pickable_objects.add(voxel)\n",
    "    objects[tuple(map(int, voxel.position))] = voxel"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "coord_html = HTML(\"Coords: ()\")\n",
    "def on_surf_mousemove(change):\n",
    "    \"\"\"\n",
    "    Show rollOver helper on mousemove\n",
    "    \"\"\"\n",
    "    rollOver_point.visible = True\n",
    "    \n",
    "    # convert to discrete coordinates\n",
    "    pos = map_to_grid(change.owner.point)\n",
    "    \n",
    "    # update rollOver helper position\n",
    "    rollOver_point.position = pos\n",
    "    \n",
    "    # write coordinates to html container\n",
    "    coord_html.value = \"Coords: (%d, %d, %d)\" % tuple(pos)\n",
    "\n",
    "def on_voxel_mousemove(change):\n",
    "    \"\"\"\n",
    "    Show rollOver helper on mousemove on voxel\n",
    "    \"\"\"\n",
    "    # convert to discrete coordinates\n",
    "    voxel = change.owner.object\n",
    "    pos = map_to_voxel_side(voxel, change.owner.faceIndex)\n",
    "    if pos is None:\n",
    "        rollOver_point.visible = False\n",
    "        return\n",
    "    rollOver_point.visible = True\n",
    "    \n",
    "    # update rollOver helper position\n",
    "    rollOver_point.position = pos\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def on_click(change):\n",
    "    if change.owner.object is surface:\n",
    "        on_surf_click(change)\n",
    "    else:\n",
    "        on_voxel_click(change)\n",
    "        \n",
    "def on_mousemove(change):\n",
    "    if change.owner.object is None:\n",
    "        # Hide rollOver if outside\n",
    "        rollOver_point.visible = False\n",
    "    elif change.owner.object is surface:\n",
    "        on_surf_mousemove(change)\n",
    "    else:\n",
    "        on_voxel_mousemove(change)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "click_picker.observe(on_click, names=['point'])\n",
    "mousemove_picker.observe(on_mousemove, names=['faceIndex'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Camera and scene"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "width = 800\n",
    "height = 600"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "code_folding": [
     0,
     5,
     12,
     23
    ]
   },
   "outputs": [],
   "source": [
    "camera = PerspectiveCamera(\n",
    "    position = [500, 800, 1300],\n",
    "    aspect = width/height,\n",
    "    fov = 35, \n",
    "    near = 1,\n",
    "    far = 10000\n",
    ")\n",
    "camera.lookAt([0,0,0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scene = Scene(\n",
    "    children = [\n",
    "        pickable_objects,\n",
    "        surf_grid,\n",
    "        rollOver_point,\n",
    "        AmbientLight(\n",
    "            color = '#606060'\n",
    "        ),\n",
    "        DirectionalLight(\n",
    "            color = '#ffffff',\n",
    "            position = normalize([1, 0.75, 0.5]),\n",
    "            intensity = 0.5\n",
    "        )\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Render the scene"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "renderer = Renderer(\n",
    "    camera = camera,\n",
    "    scene = scene,\n",
    "    controls = [\n",
    "        OrbitControls(\n",
    "            controlling = camera\n",
    "        ),\n",
    "        click_picker,\n",
    "        mousemove_picker,\n",
    "    ],\n",
    "    background = '#f0f0f0',\n",
    "    antialias = True,\n",
    "    width=width,\n",
    "    height=height,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "display(coord_html, renderer)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
